import { List } from 'immutable';
import Plugin from '../model/plugin';
import Action from '../model/action';
import Schema from '../model/schema';
import CommandsPlugin from '../plugin/command';
import QueryPlugin from '../plugin/query';

class Controller {
  constructor(options) {
    const {
      construct = true,
      value,
      plugins = [],
      onChange = () => {},
      schema = {},
      onModeChange = () => {},
      controller = this
    } = options;
    this.controller = controller;
    this.middware = {};
    this.onChange = onChange;
    this.onModeChange = onModeChange;
    const pluginArray = plugins.map(item => new Plugin(item));
    this.setValue(value);
    this.registerPlugin(pluginArray);
    this.schema = Schema.create({ ...schema, plugins: pluginArray });
    this.flushing = false;
    this.actionStack = List();
    if (construct) {
      this.run('onConstruct');
    }
  }

  setSchema(schema) {
    this.schema = schema;
  }

  setValue(value) {
    this.value = value;
  }

  applyAction(type, option) {
    const { value } = this;
    const action = Action.create({ ...option, type, value });
    this.apply(action);
  }

  apply(action) {
    const { controller } = this;
    this.value = action.apply(this.value, this.schema, controller);
    controller.saveHistory(action);
    this.actionStack = this.actionStack.push(action);
    if (!this.flushing) {
      this.flushing = true;
      Promise.resolve().then(() => this.flush());
    }
  }

  flush() {
    const { controller } = this;
    this.flushing = false;
    this.actionStack = List();
    this.onChange({ value: this.value, actions: this.actionStack });
    this.run('onChange', { value: this.value, actions: this.actionStack });
    return controller;
  }

  command = (type, ...args) => {
    const { controller } = this;
    if (typeof type === 'function') {
      type(controller, ...args);
      return;
    }
    this.run('onCommand', { type, args });
    return controller;
  };

  query = (type, ...args) => {
    const { controller } = this;
    if (typeof type == 'function') {
      return type(controller, ...args);
    }
    return this.run('onQuery', { type, args });
  };

  run = (key, ...args) => {
    const { middware, controller } = this;
    const fns = middware[key] || [];
    let i = 0;
    function next(...overrides) {
      const fn = fns[i++];
      if (!fn) {
        return;
      }
      let arugments = args;
      if (overrides.length > 0) {
        arugments = overrides;
      }
      return fn(...arugments, controller, next);
    }
    return next();
  };

  registerQuery = type => {
    const { controller } = this;
    if (type in controller && controller[type].__query) {
      return controller;
    }
    const method = (...args) => this.query(type, ...args);
    controller[type] = method;
    method.__query = true;
    return controller;
  };

  registerCommand = type => {
    const { controller } = this;
    if (type in controller && controller[type].__command) {
      return controller;
    }
    const method = (...args) => this.command(type, ...args);
    controller[type] = method;
    method.__command = true;
    return controller;
  };

  registerPlugin = plugin => {
    if (Array.isArray(plugin)) {
      plugin.map(this.registerPlugin);
      return;
    }
    if (!Plugin.isPlugin(plugin)) {
      plugin = new Plugin(plugin);
    }

    const { commands, queries, schema, ...reset } = plugin;
    if (commands) {
      const commandPlugin = CommandsPlugin(commands);
      this.registerPlugin(commandPlugin);
    }
    if (queries) {
      const queryPlugin = QueryPlugin(queries);
      this.registerPlugin(queryPlugin);
    }
    for (const key in reset) {
      const fn = plugin[key];
      const middware = this.middware[key] || [];
      middware.push(fn);
      this.middware[key] = middware;
    }
  };
}

export default Controller;
